JSON Reader and Writer classes

UTF‑16–native JSON reader/writer plus a string unquoter, designed to interop cleanly with BSTR/DWSTRING, DVARIANT, and DSafeArray.

Zero dependencies, COM‑friendly types, predictable pretty‑printing, and safe handling of escapes/control chars.

JsonUnquoteW function

Decodes a JSON string literal into a DWSTRING, resolving escape sequences and control codes.

FUNCTION JSonUnquoteW (BYREF wszJson AS WSTRING) AS DWSTRING
Parameter Description
wszJson A JSON‑encoded string surrounded by quotes.

Return value

Input/contract: Expects a JSON‑encoded string surrounded by quotes. Returns empty if the input isn’t a quoted JSON string.

Decoding rules: Handles " \ / nd (one UTF‑16 code unit per escape). Surrogate pairs pass through correctly when present as consecutive sequences.

Typical use: Consume WebView2 ExecuteScript string results (which arrive as JSON strings) and recover the plain text.


JsonReader class

Tokenizes a UTF‑16 JSON text into a stream of JsonToken entries without allocating a DOM.

Name Description
Constructor Creates a new JsonReader object initialized to the specified value.
ReadNext Reads the next token.
ReadNumber Parses a JSON number.
ReadString Reads a string by slicing the raw JSON string and unquoting via JSonUnquoteW.
SkipWhitespace Advances the position past spaces, tabs, CR and LF.

JsonWriter class

Builds JSON text into an internal BSTRING buffer with optional pretty‑printing and smart inline formatting.

Name Description
BeginArray Emits “[”, increases depth, pushes a “first item” flag, and inserts newline+indent.
BeginObject Structures control with inline suppression.
Clear Resets the buffer, depth, and firstItemStack to an empty state.
EndArray Closes the array with proper dedent and “]”, then pops the stack.
EndObject Closes the object with proper dedent and “}”.
Name Writes a JSON object key with correct escaping, colon, and spacing.
SetIndentSize Control spaces per indent level.
SetInlineThreshold Control spaces per indent level.
ToBString Returns the current buffer as BSTRING (UTF-16).
ToUtf8 Returns the current buffer as a utf-8 string.
Value Emits a JSON string with escaping for quotes, backslashes, control chars, and nonprintables via .
ValueBool Emits a boolean true or false value.
ValueNull Emits a literal null, respecting comma/indent rules.
ValueSafeArray Serializes a 1‑D SAFEARRAY as JSON array with smart inline vs pretty layout.
ValueVariant Serializes a DVARIANT using JSON‑compatible mapping.

JSON token structure

TYPE JsonToken
   kind  AS JsonTokenType
   value AS DWSTRING  ' only for string/number/bool/null
END TYPE

JSON token type enumeration

ENUM JsonTokenType
   JSON_NONE = 0
   JSON_OBJECT_START
   JSON_OBJECT_END
   JSON_ARRAY_START
   JSON_ARRAY_END
   JSON_STRING
   JSON_NUMBER
   JSON_BOOL
   JSON_NULL
   JSON_COLON
   JSON_COMMA
END ENUM

Constructor (JsonReader)

Creates a new JsonReader object initialized to the specified value.

CONSTRUCTOR JsonReader (BYREF source AS WSTRING)
Parameter Description
source A JSON‑encoded string surrounded by quotes.

Example

' Craft some JSON with tricky bits:
' - Unicode \u00E9 (é) and surrogate pair \uD83D\uDE03 (??)
' - Quotes, backslashes, slash, control codes
' - Numbers with exponents, booleans, null
DIM sample AS DWSTRING = _
    "{""name"":""Jos" & WCHR(&h00E9) & " " & WCHR(&hD83D) & WCHR(&hDE03) & """, " & _
    """quote"":""He said """"Hello/World""""!"", " & _
    """newline"":""Line1" & WCHR(10) & "Line2"", " & _
    """pi"":3.14159, ""exp"":-2.5e+3, " & _
    """ok"":true, ""missing"":null}"

DIM rdr AS JsonReader = JsonReader(sample)
DIM tok AS JsonToken
DIM idx AS LONG

WHILE rdr.ReadNext(tok)
   idx += 1
   PRINT idx; ". "; 
   SELECT CASE tok.kind
      CASE JSON_STRING
         PRINT "STRING: "; tok.value
      CASE JSON_NUMBER
         PRINT "NUMBER: "; tok.value
      CASE JSON_BOOL
         PRINT "BOOL: "; tok.value
      CASE JSON_NULL
         PRINT "NULL"
      CASE JSON_OBJECT_START
         PRINT "{"
      CASE JSON_OBJECT_END
         PRINT "}"
      CASE JSON_ARRAY_START
         PRINT "["
      CASE JSON_ARRAY_END
         PRINT "]"
      CASE JSON_COLON
         PRINT ":"
      CASE JSON_COMMA
         PRINT ","
      CASE ELSE
         PRINT "UNKNOWN"
   END SELECT
WEND

Output:

 1. {
 2. STRING: name
 3. :
 4. STRING: José 😃
 5. ,
 6. STRING: quote
 7. :
 8. STRING: He said
 9. STRING: Hello/World
 10. STRING: !
 11. ,
 12. STRING: newline
 13. :
 14. STRING: Line1
Line2
 15. ,
 16. STRING: pi
 17. :
 18. NUMBER: 3.14159
 19. ,
 20. STRING: exp
 21. :
 22. NUMBER: -2.5e+3
 23. ,
 24. STRING: ok
 25. :
 26. BOOL: true
 27. ,
 28. STRING: missing
 29. :
 30. NULL
 31. }

SkipWhitespace

Advances the position past spaces, tabs, CR, and LF.

SUB JsonReader.SkipWhitespace

ReadString

Reads a JSON string literal starting at the opening quote and returns its decoded text.

FUNCTION ReadString () AS DWSTRING

Walks raw characters, skips over escapes (including ), slices the raw quoted segment, then calls JsonUnquoteW to decode.


ReadNumber

Parses a JSON number checking for optional sign, integer, optional fraction and optional exponent.

FUNCTION ReadNumber () AS DWSTRING

Returns the exact textual representation captured.

If no digits are consumed, resets to start and returns empty. If an exponent marker lacks digits, truncates back to pre‑exponent.


ReadNext

Produces the next token and advances the position.

FUNCTION ReadNext (BYREF tok AS JsonToken) AS BOOLEAN

Tokenization:

Structural: { } [ ] : , mapped to JSON_OBJECT_START/END, JSON_ARRAY_START/END, JSON_COLON, JSON_COMMA.

Values: JSON_STRING via ReadString, JSON_NUMBER via ReadNumber, JSON_BOOL (“true”/“false”), JSON_NULL (“null”).

End/invalid handling: Returns FALSE at end of buffer. On unrecognized input, sets JSON_NONE and fast‑forwards to avoid infinite loops.


SetIndentSize

Sets the maximum character length used to decide if arrays/objects may be emitted inline.

SUB SetIndentSize(BYVAL n As LONG)
Parameter Description
n The number of spaces to indent.

SetInlineThreshold

Sets the maximum character length used to decide if arrays/objects may be emitted inline.

SUB SetInlineThreshold(BYVAL n As LONG)
Parameter Description
n The maximum character length.

BeginObject

Structures control with inline suppression.

SUB BeginObject

Purpose: Emits “{”, increases depth, pushes a “first item” flag, and inserts newline+indent.

Examples

Primitives + string escaping

#include once "AfxNova/AfxJson.inc"
USING AfxNova

DIM esc AS DWSTRING = "Quote: """ & " Newline: " & WCHR(10) & "Tab: " & WCHR(9) & "End"
DIM jw AS JsonWriter
jw.SetIndentSize(2)
jw.BeginObject()
  jw.Name("s")    : jw.Value(esc)
  jw.Name("i64")  : jw.Value(9223372036854775807)  ' max signed 64
  jw.Name("pi")   : jw.Value(3.141592653589793)
  jw.Name("ok")   : jw.ValueVariant(DVARIANT(TRUE, "BOOLEAN"))   ' VT_BOOL -> true
  jw.Name("none") : jw.ValueVariant(DVARIANT())                  ' VT_EMPTY -> null
jw.EndObject()
print jw.ToUtf8

Nested objects

#include once "AfxNova/AfxJson.inc"
USING AfxNova

Dim jw As JsonWriter
jw.SetIndentSize(2)
jw.BeginObject()
  jw.Name("app") : jw.Value("AfxNova")
  jw.Name("version") : jw.Value(1)
  jw.Name("author")
  jw.BeginObject()
    jw.Name("name")  : jw.Value("José Roca")
    jw.Name("site")  : jw.Value("https://github.com/JoseRoca/AfxNova")
    jw.Name("active"): jw.ValueVariant(DVARIANT(TRUE, "BOOLEAN"))
  jw.EndObject()
  jw.Name("notes") : jw.Value("All UTF-16 internally")
jw.EndObject()
print jw.ToBString

EndObject

Closes the object with proper dedent and “}”, then pops the “first item” stack.

SUB EndObject

BeginArray

Emits “[”, increases depth, pushes a “first item” flag, and inserts newline+indent.

SUB BeginArray

EndArray

Closes the array with proper dedent and “]”, then pops the stack.

SUB EndArray

Name

Writes a JSON object key with correct escaping, colon, and spacing.

SUB Name (BYREF s AS WSTRING)
Parameter Description
s The JSON string.

Resets the current level’s “first item” flag so the next value decides comma/newline.


Value

Emits a JSON string with escaping for quotes, backslashes, control chars, and nonprintables via .

SUB Value (BYREF s AS WSTRING)
SUB Value (BYVAL n AS LONGINT)
SUB Value (BYVAL n AS DOUBLE)
Parameter Description
s The string to emit.
n The number to emit.

The numbers are converted to string with WSTR.

Examples

Unicode text (BMP): accents, em dash, Greek

#include once "AfxNova/AfxJson.inc"
USING AfxNova

DIM uni AS DWSTRING = "España — café — ?x"
DIM jw AS JsonWriter
jw.SetIndentSize(2)
jw.BeginObject()
  jw.Name("title") : jw.Value("Unicode test")
  jw.Name("text")  : jw.Value(uni)
jw.EndObject()
AfxMsg jw.ToBString

ValueNull

Emits a literal null, respecting comma/indent rules.

SUB ValueNull

ValueBool

Emits a boolean true or false value.

SUB ValueBool (BYVAL b AS BOOLEAN)
Parameter Description
b A boolean true(-1) or false(0) value.

ValueVariant

Serializes a DVARIANT using JSON‑compatible mapping.

SUB ValueVariant (BYREF dv AS DVARIANT)
Parameter Description
*dv A variant value.

Mapping :

Example

#include once "AfxNova/AfxJson.inc"
USING AfxNova

DIM jw AS JsonWriter
jw.SetIndentSize(0) ' 0 = no pretty-print
jw.BeginObject()
  jw.Name("k")  : jw.Value("v")
  jw.Name("n")  : jw.Value(123)
  jw.Name("b")  : jw.ValueVariant(DVARIANT(FALSE, "BOOLEAN"))
  jw.Name("nil"): jw.ValueVariant(DVARIANT())
jw.EndObject()
print jw.ToBString

ValueSafeArray

Serializes a 1‑D SAFEARRAY as JSON array with smart inline vs pretty layout.

SUB ValueSafeArray (BYREF sa AS DSafeArray)

Escapes VT_BSTR directly; otherwise serializes each element with ValueVariant.

Example

SAFEARRAY serialization with inline suppression.

#include once "AfxNova/AfxJson.inc"
USING AfxNova

DIM dvArr AS DSafeArray
dvArr.Create(VT_VARIANT, 3, 0)
dvArr.PutVar(0, DVARIANT("first ??"))
dvArr.PutVar(1, DVARIANT(42))
dvArr.PutVar(2, DVARIANT(3.14159))
DIM jw AS JsonWriter
jw.SetIndentSize(2) ' 2-space indent
jw.BeginObject()
  jw.Name("title") : jw.Value("Test")
  jw.Name("data")  : jw.ValueSafeArray(dvArr)
jw.EndObject()
PRINT jw.ToBString()

' Outout:
{
  "title": "Test",
  "data": [
    "first",
    42,
    3.14159
  ]
}
#include once "AfxNova/AfxJson.inc"
USING AfxNova

DIM tiny AS DSafeArray
tiny.Create(VT_VARIANT, 3, 0)
tiny.PutVar(0, DVARIANT(1))
tiny.PutVar(1, DVARIANT(2))
tiny.PutVar(2, DVARIANT(3))
DIM jw As JsonWriter
jw.SetIndentSize(2)
jw.SetInlineThreshold(20)  ' tighten threshold
jw.BeginObject()
   jw.Name("a") : jw.ValueSafeArray(tiny)
   jw.Name("b") : jw.Value("longer text here will break inline")
jw.EndObject()

Output:
{
 "a": [
   1,
   2,
   3
 ],
 "b": "longer text here will break inline"
}

ToBString

Returns the current buffer as a BSTRING (UTF‑16).

FUNCTION ToBString () AS BSTRING

ToUtf8

Returns the current buffer as a UTF-8 string.

FUNCTION ToUtf8 () AS STRING

Clear

Resets the buffer, depth, and m_firstItemStack to an empty state.

SUB Clear

Notes and tips

WebView2 bridge: Use ToBString with PostWebMessageAsJson for nativeJSON, and JSonUnquoteW for ExecuteScript string returns.

Pretty vs compact: SetIndentSize(0) for compact output; rely on m_inlineThreshold to keep small arrays inline even with pretty‑print enabled.

Robust reading: JsonReader is a tokenizer, not a full validator. Guard your consumer against JSON_NONE to handle malformed input.

Unicode fidelity: Writer escapes control characters and preserves BMP (Basic Multilingual Plane) characters directly; astral plane characters in input strings are emitted as UTF‑16 code units and will be round‑tripped correctly by modern JS engines.

They’re built first and foremost to make life easier when shuttling data in and out of WebView2, but because they stick to clean JSON in/out and COM‑friendly types, they’re essentially drop‑in utilities anywhere you need structured text parsing or emission.

That means you could:

Feed them API responses from a local service or a remote REST endpoint

Serialize/deserialize data between FreeBASIC apps without pulling in heavier libraries

Store application state in a readable format for logs or config files

Transform Excel/Access/other COM‑capable app data into JSON for reporting or exchange